﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Configuration;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>
    /// A field whose begin and end values users can change to create a dynamic filter for <see cref="SearchEngine"/>.
    /// </summary>
    [Serializable]
    public sealed class SearchField : ISearchEngineField
    {
        #region Configuration.

        /// <summary>
        /// Default setting for <see cref="IgnoreTimePart"/> property.
        /// </summary>
        /// <value><b>true</b> to ignore time part; <b>false</b> otherwise. Default is <b>false</b>.</value>
        /// <remarks>Settings are read from the application's config file as shown in the following example:
        /// <code>
        /// <add key="BizElements.BusinessLayer.SearchField.IgnoreTimePart" value="true"/>
        /// </code>
        /// </remarks>
        public static readonly bool IgnoreTimePartDefaultSetting;

        static SearchField()
        {
            IgnoreTimePartDefaultSetting = ("true" == ConfigurationManager.AppSettings["BizElements.BusinessLayer.SearchField.IgnoreTimePart"]);
        }

        #endregion

        #region Fields.

        IDbColumn field;
        ICodeList allowedValues;
        bool allowIntervalSearch;
        object beginValue;
        object endValue;
        string caption;
        int ordinal;
        object[] values;
        bool negate;
        bool isAutoPopulated;
        bool addWithOr;
        string description;
        bool isHidden;
        string beginValueComparisonOperator;
        bool allowArbitraryBeginValue = true;
        IList<INamedValueSet> valueSets = new List<INamedValueSet>();
        string selectedValueSetKey;
        bool allowMultipleValues = true;
        TextSearchMode[] allowedTextSearchModes = new TextSearchMode[] { TextSearchMode.Default, TextSearchMode.BeginningOfText, TextSearchMode.ExactMatch, TextSearchMode.AnywhereInText };
        TextSearchMode selectedTextSearchMode = TextSearchMode.Default;
        bool useFullTextIndex;
        bool isNull;
        bool ignoreTimePart;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of <b>SearchField</b> class.
        /// </summary>
        /// <param name="field">Database column whose data is compared against the values of this <b>SearchField</b>.</param>
        /// <param name="caption">Field caption.</param>
        public SearchField(IDbColumn field, string caption)
            : this(field, caption, null, null, true)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>SearchField</b> class.
        /// </summary>
        /// <param name="field">Database column whose data is compared against the values of this <b>SearchField</b>.</param>
        /// <param name="caption">Field caption.</param>
        /// <param name="allowIntervalSearch">Indicates whether interval search is allowed for this field.</param>
        public SearchField(IDbColumn field, string caption, bool allowIntervalSearch)
            : this(field, caption, null, null, allowIntervalSearch)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>SearchField</b> class.
        /// </summary>
        /// <param name="field">Database column whose data is compared against the values of this <b>SearchField</b>.</param>
        /// <param name="caption">Field caption.</param>
        /// <param name="allowedValues">List of allowed values a client can choose from.</param>
        public SearchField(IDbColumn field, string caption, ICodeList allowedValues)
            : this(field, caption, allowedValues, null, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>SearchField</b> class.
        /// </summary>
        /// <param name="field">Database column whose data is compared against the values of this <b>SearchField</b>.</param>
        /// <param name="caption">Field caption.</param>
        /// <param name="allowedValues">List of allowed values a client can choose from.</param>
        /// <param name="allowIntervalSearch">Indicates whether interval search is allowed for this field.</param>
        public SearchField(IDbColumn field, string caption, ICodeList allowedValues, bool allowIntervalSearch)
            : this(field, caption, allowedValues, null, allowIntervalSearch)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>SearchField</b> class.
        /// </summary>
        /// <param name="field">Database column whose data is compared against the values of this <b>SearchField</b>.</param>
        /// <param name="caption">Field caption.</param>
        /// <param name="lookupBuilder">Builds engine which searches for allowed field values.</param>
        public SearchField(IDbColumn field, string caption, ISearchEngineBuilder lookupBuilder)
            : this(field, caption, null, lookupBuilder, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of <b>SearchField</b> class.
        /// </summary>
        /// <param name="field">Database column whose data is compared against the values of this <b>SearchField</b>.</param>
        /// <param name="caption">Field caption.</param>
        /// <param name="lookupBuilder">Builds engine which searches for allowed field values.</param>
        /// <param name="allowIntervalSearch">Indicates whether interval search is allowed for this field.</param>
        public SearchField(IDbColumn field, string caption, ISearchEngineBuilder lookupBuilder, bool allowIntervalSearch)
            : this(field, caption, null, lookupBuilder, allowIntervalSearch)
        {
        }

        private SearchField(IDbColumn field, string caption, ICodeList allowedValues, ISearchEngineBuilder lookupBuilder, bool allowIntervalSearch)
        {
            this.ignoreTimePart = IgnoreTimePartDefaultSetting;
            this.caption = caption;
            this.field = field;
            this.allowIntervalSearch = allowIntervalSearch;
            this.allowedValues = allowedValues;
            this.LookupBuilder = lookupBuilder;
        }

        #endregion

        #region CreatePredicate.

        /// <summary>
        /// Creates a predicate expression or search condtion for the engine's dynamic filter.
        /// </summary>
        /// <returns><b>PredicateBase</b> or <b>SearchCondition</b> which compares the data in the database columns against the values user entered in this search field.</returns>
        internal object CreatePredicate()
        {
            PredicateBase predicate = null;
            SearchCondition subWhere = null;
            INamedValueSet selectedValueSet = GetSelectedValueSet();

            if (selectedValueSet != null)
            {
                predicate = CreatePredicateForSelectedValueSet(selectedValueSet);
            }
            else if (this.isNull)
            {
                predicate = PredicateFactory.IsNull(this.field, this.negate);
            }
            else
            {
                if (this.values != null && this.values.Length > 0)
                {
                    CreateFilterForMultipleValues(ref subWhere, ref predicate);
                }
                else if (this.beginValue != null)
                {
                    if (this.endValue == null)
                        predicate = CreatePredicateForSingleValue();
                    else
                        predicate = CreateBetweenPredicate();
                }
            }

            object filter = null;
            if (predicate != null)
                filter = predicate;
            else if (subWhere != null && !subWhere.IsEmpty)
                filter = subWhere;

            return filter;
        }

        private PredicateBase CreatePredicateForSingleValue()
        {
            PredicateBase predicate = null;
            if (IsStringField())
            {
                switch (this.selectedTextSearchMode)
                {
                    case TextSearchMode.BeginningOfText:
                        predicate = PredicateFactory.Like(this.field, this.beginValue.ToString() + "%", this.negate);
                        break;

                    case TextSearchMode.ExactMatch:
                        predicate = PredicateFactory.Compare(this.field, "=", this.beginValue, this.negate);
                        break;

                    case TextSearchMode.AnywhereInText:
                        if (this.useFullTextIndex)
                            predicate = PredicateFactory.ContainsPrefix(this.field, this.beginValue.ToString(), this.negate);
                        else
                            predicate = PredicateFactory.Like(this.field, "%" + this.beginValue.ToString() + "%", this.negate);
                        break;

                    default:
                        if (this.beginValueComparisonOperator != null)
                            predicate = PredicateFactory.Compare(this.field, this.beginValueComparisonOperator, this.beginValue, this.negate);
                        else
                            predicate = PredicateFactory.Like(this.field, this.beginValue.ToString() + "%", this.negate);
                        break;
                }
            }
            else if (this.ignoreTimePart && IsDateField() && this.beginValue is DateTime)
            {
                DateTime dateValue = (DateTime)this.beginValue;
                DateTime zeroAm = dateValue.Date;
                DateTime midnight = dateValue.Date.AddDays(1).AddMinutes(-1);
                predicate = PredicateFactory.Between(this.field, zeroAm, midnight);
            }
            else
            {
                string op = (this.beginValueComparisonOperator != null) ? this.beginValueComparisonOperator : "=";
                predicate = PredicateFactory.Compare(this.field, op, this.beginValue, this.negate);
            }
            return predicate;
        }

        private PredicateBase CreateBetweenPredicate()
        {
            BetweenPredicate between;
            if (this.ignoreTimePart && IsDateField() && this.beginValue is DateTime && this.endValue is DateTime)
            {
                DateTime fromDateAtZeroAm = (DateTime)this.beginValue;
                fromDateAtZeroAm = fromDateAtZeroAm.Date;
                DateTime tillDateAtEndOfDay = (DateTime)this.endValue;
                tillDateAtEndOfDay = tillDateAtEndOfDay.Date.AddDays(1).AddMinutes(-1);
                between = PredicateFactory.Between(this.field, fromDateAtZeroAm, tillDateAtEndOfDay, this.negate);
            }
            else
            {
                between = PredicateFactory.Between(this.field, this.beginValue, this.endValue, this.negate);
            }

            return between;
        }

        private void CreateFilterForMultipleValues(ref SearchCondition subWhere, ref PredicateBase predicate)
        {
            bool likeOrLikeFilter = IsStringField() && (this.SelectedTextSearchMode == TextSearchMode.AnywhereInText || this.SelectedTextSearchMode == TextSearchMode.BeginningOfText);
            if (likeOrLikeFilter)
            {
                subWhere = new SearchCondition();
                foreach (object val in this.values)
                {
                    if (val == null)
                        continue;

                    string likePattern = (this.SelectedTextSearchMode == TextSearchMode.AnywhereInText) ? "%" + val.ToString() + "%" : val.ToString() + "%";
                    if (!this.negate)
                    {
                        if (this.UseFullTextIndex)
                            subWhere.Or(PredicateFactory.ContainsPrefix(this.field, val.ToString()));
                        else
                            subWhere.Or(PredicateFactory.Like(this.field, likePattern));
                    }
                    else
                    {
                        if (this.UseFullTextIndex)
                            subWhere.And(PredicateFactory.ContainsPrefix(this.field, val.ToString(), true));
                        else
                            subWhere.And(PredicateFactory.Like(this.field, likePattern, true));
                    }
                }
            }
            else
            {
                predicate = PredicateFactory.In(this.field, this.values, this.negate);
            }
        }

        private PredicateBase CreatePredicateForSelectedValueSet(INamedValueSet selectedValueSet)
        {
            PredicateBase predicate = null;
            switch (selectedValueSet.ValueSetType)
            {
                case NamedValueSetType.SingleValue:
                    object singleValue = selectedValueSet.GetSingleValue();
                    if (singleValue != null)
                    {
                        if (this.beginValueComparisonOperator != null)
                            predicate = PredicateFactory.Compare(this.field, this.beginValueComparisonOperator, singleValue, this.negate);
                        else if (IsStringField())
                            predicate = PredicateFactory.Like(this.field, singleValue.ToString() + "%", this.negate);
                        else
                            predicate = PredicateFactory.Compare(this.field, "=", singleValue, this.negate);
                    }
                    break;

                case NamedValueSetType.Interval:
                    object intervalBegin, intervalEnd;
                    selectedValueSet.GetInterval(out intervalBegin, out intervalEnd);
                    if (intervalBegin != null && intervalEnd != null)
                    {
                        predicate = PredicateFactory.Between(this.field, intervalBegin, intervalEnd, this.negate);
                    }
                    break;

                case NamedValueSetType.ListOfValues:
                    Array listOfValues = selectedValueSet.GetListOfValues();
                    if (listOfValues.Length > 0)
                    {
                        predicate = PredicateFactory.In(this.field, listOfValues, this.negate);
                    }
                    break;

                default:
                    throw new NotSupportedException("Specified value set type (" + selectedValueSet.ValueSetType.ToString() + ") is not supported.");
            }

            return predicate;
        }

        private INamedValueSet GetSelectedValueSet()
        {
            if (!string.IsNullOrEmpty(this.SelectedValueSetKey))
            {
                foreach (INamedValueSet valueSet in this.ValueSets)
                {
                    if (valueSet.Key == this.SelectedValueSetKey)
                        return valueSet;
                }
            }

            return null;
        }

        private bool IsStringField()
        {
            Type t = this.field.DataType;
            if (t == typeof(string))
                return true;
            else
                return false;
        }

        private bool IsDateField()
        {
            return TypeUtil.IsDate(this.field.DataType);
        }

        #endregion

        #region BizElements specific properties.

        /// <summary>
        /// Gets or sets the database column whose data is compared against the values of this <b>SearchField</b>.
        /// </summary>
        public IDbColumn Field
        {
            get { return this.field; }
            set { this.field = value; }
        }

        /// <summary>
        /// Gets or sets the comparison operator which the engine will be forced to use when creating
        /// a predicate for the <see cref="BeginValue"/> property. Default is <b>null</b>.
        /// </summary>
        /// <remarks>If set to <b>null</b> the engine will use <b>LIKE</b> operator for strings and 
        /// <b>=</b> operator for all other data types. Has no meaning if <see cref="EndValue"/> 
        /// property is set because in that case interval comparison (BETWEEN) is used.</remarks>
        public string BeginValueComparisonOperator
        {
            get { return this.beginValueComparisonOperator; }
            set { this.beginValueComparisonOperator = value; }
        }

        /// <summary>
        /// Gets or sets whether specialized text indexes, ie. <b>ContainsPredicate</b> objects, are to be used when in <see cref="TextSearchMode.AnywhereInText"/> mode.
        /// </summary>
        /// <value><b>true</b> to create CONTAINS predicates; <b>false</b> to create LIKE predicates. Default is <b>false</b>.</value>
        public bool UseFullTextIndex
        {
            get { return this.useFullTextIndex; }
            set { this.useFullTextIndex = value; }
        }

        /// <summary>
        /// Gets or sets whether time parts are ignored when comparing date fields, i.e. whether an interval comparison (00:00 - 23:59) is used.
        /// </summary>
        /// <value><b>true</b> to ignore time part; <b>false</b> otherwise. Default value is configured in <see cref="IgnoreTimePartDefaultSetting"/> property.</value>
        public bool IgnoreTimePart
        {
            get { return this.ignoreTimePart; }
            set { this.ignoreTimePart = value; }
        }

        #endregion

        #region IBindableField.

        /// <summary>
        /// Gets the name/alias of the database field which is used to bind it to a UI control.
        /// </summary>
        public string BindingName
        {
            get { return this.field.Alias; }
        }

        /// <summary>
        /// Gets or sets field caption displayed in UI layer.
        /// </summary>
        public string Caption
        {
            get { return this.caption; }
            set { this.caption = value; }
        }

        /// <summary>
        /// Gets the .NET type of the values stored in the database column.
        /// </summary>
        public Type DataType
        {
            get { return this.field.DataType; }
        }

        /// <summary>
        /// Gets the value indicating whether the contents of the field may be modified. Always returns <b>false</b>.
        /// </summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Gets or sets the list of allowed values a client can choose from. <b>Nullable</b>.
        /// </summary>
        public ICodeList CodeList
        {
            get { return this.allowedValues; }
            set { this.allowedValues = value; }
        }

        /// <summary>
        /// Gets the value which indicates whether the field is nullable. Always <b>true</b>.
        /// </summary>
        public bool IsNullable
        {
            get { return true; }
        }

        /// <summary>
        /// Not implemented. Returns <b>null</b>.
        /// </summary>
        public string[] SatelliteFields
        {
            get { return null; }
            set { }
        }

        /// <summary>
        /// Gets or sets the position of the field in the collection. 
        /// </summary>
        public int Ordinal
        {
            get { return this.ordinal; }
            set { this.ordinal = value; }
        }

        /// <summary>
        /// Gets or ses the description of the bound field. <b>Nullable</b>.
        /// </summary>
        public string Description
        {
            get { return this.description; }
            set { this.description = value; }
        }

        /// <summary>
        /// Gets or sets the value which indicates whether the field should be hidden from view in the user interface.
        /// </summary>
        public bool IsHidden
        {
            get { return this.isHidden; }
            set { this.isHidden = value; }
        }

        #endregion

        #region ISearchEngineField properties.

        /// <summary>
        /// Gets or sets the value to search for in the data-source.
        /// </summary>
        public object BeginValue
        {
            get { return this.beginValue; }
            set { this.beginValue = value; }
        }

        /// <summary>
        /// Gets or sets the end value of the interval to search for in the data-source.
        /// </summary>
        public object EndValue
        {
            get { return this.endValue; }
            set
            {
                if (this.allowIntervalSearch)
                    this.endValue = value;
                else
                    this.endValue = null;
            }
        }

        /// <summary>
        /// Gets  or sets the value which indicates whether interval search is allowed for this field.
        /// </summary>
        /// <remarks>When set to <b>false</b> the <see cref="EndValue"/> is ignored.
        /// UI layer shouldn't allow users to enter end values if interval search is not supported.</remarks>
        public bool AllowIntervalSearch
        {
            get { return this.allowIntervalSearch; }
            set { this.allowIntervalSearch = value; }
        }

        /// <summary>
        /// Gets or sets values to search for in the data-source. If this property is set then 
        /// <see cref="BeginValue"/> and <see cref="EndValue"/> properties are ignored.
        /// </summary>
        public object[] Values
        {
            get { return this.values; }
            set { this.values = value; }
        }

        /// <summary>
        /// Gets or sets the value which indicates that the field should be automatically populated by 
        /// application. User shouldn't be allowed to enter arbitrary values.
        /// </summary>
        public bool IsAutoPopulated
        {
            get { return this.isAutoPopulated; }
            set { this.isAutoPopulated = value; }
        }

        /// <summary>
        /// Gets or sets the value which indicates whether the filter created for this field is inverted.
        /// </summary>
        /// <remarks>Eg. all values except those in the interval defined by <see cref="BeginValue"/>
        /// and <see cref="EndValue"/> properties match the filter.</remarks>
        public bool Negate
        {
            get { return this.negate; }
            set { this.negate = value; }
        }

        /// <summary>
        /// Gets or sets the value which indicates whether the filter created for this field is combined using logical OR.
        /// </summary>
        public bool AddWithOr
        {
            get { return this.addWithOr; }
            set { this.addWithOr = value; }
        }

        /// <summary>
        /// Gets or sets the values which indicates whether arbitrary values are allowed.
        /// </summary>
        /// <remarks>When set to <b>false</b> the UI layer shouldn't allow users to enter arbitrary begin values.
        /// However, the engine should provide some other mechanism which enables user's to set values or 
        /// intervals, eg. <see cref="IBindableField.CodeList"/> or <see cref="ValueSets"/>.</remarks>
        public bool AllowArbitraryBeginValue
        {
            get { return this.allowArbitraryBeginValue; }
            set { this.allowArbitraryBeginValue = value; }
        }

        /// <summary>
        /// Gets or sets the value which specifies use cases for the current field.
        /// </summary>
        public SearchFieldUsage Usage { get; set; }

        /// <summary>
        /// Gets or sets predefined sets of values/intervals for this field.
        /// </summary>
        public IList<INamedValueSet> ValueSets
        {
            get { return this.valueSets; }
            set { this.valueSets = value; }
        }

        /// <summary>
        /// Gets or sets the key (see <see cref="INamedValueSet.Key"/>) of a selected value set.
        /// </summary>
        public string SelectedValueSetKey
        {
            get { return this.selectedValueSetKey; }
            set { this.selectedValueSetKey = value; }
        }

        /// <summary>
        /// Gets or sets whether a user may specify multiple values to search for through <see cref="Values"/> property.
        /// </summary>
        /// <value><b>true</b> to allow multiple values, <b>false</b> otherwise. Default is <b>true</b>.</value>
        public bool AllowMultipleValues
        {
            get { return this.allowMultipleValues; }
            set { this.allowMultipleValues = value; }
        }

        /// <summary>
        /// Gets or sets which text search modes are available to the user.
        /// </summary>
        /// <value>Array of <see cref="TextSearchMode"/> or <b>null</b>. If <b>null</b> and empty arrays are
        /// converted to an array which contains only <see cref="TextSearchMode.Default"/> mode.
        /// By default all modes are allowed.</value>
        public TextSearchMode[] AllowedTextSearchModes
        {
            get { return this.allowedTextSearchModes; }
            set
            {
                if (value == null || value.Length == 0)
                    value = new TextSearchMode[] { TextSearchMode.Default };

                this.allowedTextSearchModes = value;
            }
        }

        /// <summary>
        /// Gets or sets which of the allowed <see cref="TextSearchMode"/> to use when creating a filter.
        /// </summary>
        /// <value>One of the values defined in the <see cref="AllowedTextSearchModes"/>. Default is
        /// <see cref="TextSearchMode.Default"/>.</value>
        public TextSearchMode SelectedTextSearchMode
        {
            get { return this.selectedTextSearchMode; }
            set
            {
                bool notAllowed = (Array.IndexOf<TextSearchMode>(this.allowedTextSearchModes, value) < 0);
                if (notAllowed)
                    throw new ArgumentOutOfRangeException("value", "The selected text search mode is not allowed.");

                this.selectedTextSearchMode = value;
            }
        }

        /// <summary>
        /// Gets or sets whether to return only records where the field value is not set, ie. <b>null</b>.
        /// </summary>
        /// <value><b>true</b> to return only records where the field value is not set, ie. <b>null</b>; otherwise <b>false</b>. Default is <b>false</b>.</value>
        /// <remarks>If this property is set to <b>true</b> then <see cref="BeginValue"/>, <see cref="EndValue"/> and <see cref="Values"/> properties are ignored.</remarks>
        public bool IsNull
        {
            get { return this.isNull; }
            set { this.isNull = value; }
        }

        #endregion

        #region LookupBuilder.

        string engineBuilderAssembly;
        string engineBuilderClass;

        /// <summary>
        /// Gets or sets the object that builds engine which searches for allowed field values.
        /// </summary>
        public ISearchEngineBuilder LookupBuilder
        {
            get
            {
                ISearchEngineBuilder builder = null;
                bool assembyAndClassNamesAreSet = (this.engineBuilderAssembly != null && this.engineBuilderClass != null);
                if (assembyAndClassNamesAreSet)
                    builder = (ISearchEngineBuilder)Assembly.Load(this.engineBuilderAssembly).CreateInstance(this.engineBuilderClass);

                return builder;
            }
            set
            {
                if (value != null)
                {
                    this.engineBuilderAssembly = value.GetType().Assembly.FullName;
                    this.engineBuilderClass = value.GetType().FullName;
                }
                else
                {
                    this.engineBuilderAssembly = null;
                    this.engineBuilderClass = null;
                }
            }
        }

        #endregion
    }
}
